Skip to content

fix(spec,objectql): declare the inert-JSON artifact and registry-record package body stages, and stop the record under-reporting functions - #19373

Merged
os-litant merged 17 commits into
mainfrom
claude/issue-17518-assembled-package-body-inert-json
Sep 22, 2026
Merged

os-litant merged 17 commits into
mainfrom
claude/issue-17518-assembled-package-body-inert-json

Conversation

@os-litant

@os-litant os-litant commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17518

Clause-②: yes

Executes ruling A′ — decision batch #192 item 3, comment 5748934194, maintainer 「192 同意」. Its two steps, its refusals (A and B) and its fences are followed as written; every place where the tree made me read the ruling rather than transcribe it is called out below.

Base of every reading in this body: regeneration commit 96dd3549ff6, the head of the SIXTH merge.

⚠️ The readings below were brought to this head by the seat, not by the round that first wrote them. Two merge rounds have run since the first draft. Each figure corrected here is named in the correcting round's own report on card #17518 — comment 5750725852 for the first, 5750987577 for the second — and the seat re-verified the head, the regenerated index and mergeability itself before editing. Anything not listed in those two reports is the original round's reading, unchanged.

The confidence gap the ruling asked me to close first

「whether effect is required or defaulted on the declaration schema — read it, ⛔ do not mint a value」

Defaulted. FlowFunctionDeclarationSchema.effect is FlowFunctionEffectSchema.default(DEFAULT_FLOW_FUNCTION_EFFECT) where that constant is 'pure' (automation/flow-function.zod.ts). Measured, not read off the source alone: FlowFunctionLoweredDeclarationSchema.safeParse({ handler: 'x' }) succeeds and yields { handler: 'x', effect: 'pure' }. The array member of functions states FlowFunctionEffectSchema.optional() with no default, so the two forms differ and neither is restated anywhere in this diff — each JSON stage inherits its form's own optionality by deriving from it.

That reading is what the producer writes: the bare-callable normalisation uses DEFAULT_FLOW_FUNCTION_EFFECT and the array form gets nothing.

What landed

packages/spec/src/automation/flow-function.zod.tsFlowFunctionLoweredDeclarationSchema is exported (step 1), with its FlowFunctionLoweredDeclaration / …Parsed aliases. It was a module-local const, and automation/index.ts's export * only re-exports what is already exported.

packages/spec/src/stack.zod.ts — two new bodies beside AssembledPackageBodySchema:

  • ArtifactStagePackageBodySchema — the on-disk artifact stage. functions entries are the lowered spellings, hooks[].handler is a string.
  • RecordStagePackageBodySchema — the registry record stage: literally ArtifactStagePackageBodySchema.extend({ functions: … }) with functions[].handler optional in both the map-record form and the array form, and nothing else.

AssembledPackageBodySchema, composeStacks and the cannot drift invariant are ⛔ untouched: those callables are live on the stage the assembled body declares itself for, and narrowing it would refuse a published composition function's own output. Both new schemas carry the same structural z.ZodType annotation as the assembled body, for the two reasons recorded there (TS7056; a named alias turning stack.zod into a shared chunk).

packages/spec/src/api/package-api.zod.ts — the installed-package row's manifest is rebound to the record stage (step 1). The z.unknown() override and the docblock defending it are gone, and the sentence that ruling A step 5 assigns to this edit is corrected in place: those two members are not why ArtifactPackageSchema and ObjectStackDefinitionSchema publish no JSON Schema — src/stack.zod.ts is not one of the subpath namespaces build-schemas.ts walks, so neither is ever reached by the emit loop.

packages/objectql/src/registry.ts — step 2. withDeclaredFunctionEntries rewrites a bare callable functions map entry to { handler, effect: DEFAULT_FLOW_FUNCTION_EFFECT } at the assembly boundary, before toRecordManifest runs. toRecordManifest's structural rule is ⛔ untouched and no key is special-cased inside the projection; the two spellings are simply made structurally equal ahead of it. ⛔ No ref is minted, ⛔ no entry is dropped. The caller's manifest is never mutated and a copy is made only when an entry really needed rewriting.

Two places where I read the ruling rather than transcribed it — both stated so they can be overruled

  1. functions entries the lowered declaration」 is implemented as BOTH lowered members of FlowFunctionEntrySchema, not only the record one. objectstack build emits { myFn: 'myFn' } for a bare entry and { myFn: { handler: 'myFn', effect } } for a declared one, so a stage admitting only the record form would refuse artifacts this repo really writes — the failure mode that withdrew letter B, one key across. Ruling A′'s own step-4 control names both shapes (「a string and a lowered record」). Measured: the artifact stage accepts a body carrying one of each.
  2. The array member is transcribed, not derived. functions' array branch is declared inline inside the assembled body's own shape, and narrowing it in place is the one thing this pair may not do. The transcription's drift is guarded instead: stack-json-stage-package-body.test.ts pins the authoring array entry's key set equal to both JSON stages', so a key added there and not here reddens by name.

Acceptance, as ruling A′ lists it

criterion result
both bodies convert under z.toJSONSchema (self-test over the whole body) YES / YES; control: the assembled body still NO (Function types cannot be represented in JSON Schema); probe controls lit z.string() YES, dark z.object({a: z.function()}) NO
the showcase-shaped manifest (config.ts:244-249) reports 2 functions on the GET /packages row, the bare one as a handler-less declaration 2: {"summarizeCompletedTask":{"effect":"pure"},"sweepProjectHealth":{"effect":"writes"}}, driven through the real SchemaRegistry.installPackage
hooks unchanged unchanged: an inline handler is dropped (the key is optional and admits that), a string handler survives verbatim. The array functions form also keeps its entry: [{"name":"syncBilling","effect":"writes"}]
AssembledPackageBodySchema / composeStacks / the invariant untouched untouched — no edit in those regions; assembled-package-body.test.ts and compose-stacks-manifest-preserve.test.ts stay green
the two noted, not filed corrections in the same edit baseline reason line: made TRUE by step 1 rather than reworded — automation/FlowFunctionLoweredDeclaration is now in json-schema.manifest/automation.json, so 「the lowered record … publishes normally」 is now a fact. package-api.zod.ts docblock last sentence: corrected in place, see above

Stage separation, measured rather than asserted: the record stage accepts the handler-less declaration and the artifact stage refuses it; the assembled body accepts a live callable and both JSON stages refuse it; both JSON stages still refuse an authoring glob and an unknown key (namesapce). So the two keys moved from unknown to a declaration, and nothing else moved.

Reverse verification — two ablations, each restored with proof

Both ran against committed code, each with a trap restore, an on-disk landing proof (anchor grep -c before/after plus a blob-hash change) and a restore proof (git hash-object back to the HEAD blob, git diff HEAD empty).

  • A1 — remove the producer normalisation (toRecordManifest(withDeclaredFunctionEntries(manifest))toRecordManifest(manifest); anchor 1→0, injected 1, blob b0af60d7…17b7c93c…): registry-package-manifest-serializable.test.ts goes 1 failed / 15 passed, naming the exact defect — expected [ 'sweepProjectHealth' ] to deeply equal [ 'summarizeCompletedTask', …(1) ]. Restored blob b0af60d7…, diff empty.
  • A3 — collapse the record stage into the artifact stage (jsonStageFunctionsKey(true)(false); anchor 1→0, injected 2, blob 60c13b43…822bed8e…): 2 failed / 79 passed across two files — record accepts the handler-less declaration; ⛔ the ARTIFACT stage refuses it and parses a row carrying the residual the projection really produces. So the one-key difference that IS the fourth stage is load-bearing in both packages' pins. Restored blob 60c13b43…, diff empty.

No ablation is offered for 「both bodies convert」: that claim already carries its discriminating control inside the same test file (the assembled body must NOT convert), which is a lit/dark pair rather than an assertion about itself.

Tests and gates

All through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-17518, verdicts read from the wrapper's own VERDICT command-exit line and never a bare $?; every exit code captured before any pipe. Wall-clock figures in the logs are SHARED-BOX seconds.

  • pnpm --filter @objectstack/spec test513 files / 14971 tests passed, 1 todo — the FULL suite, re-run on this head because the sixth merge carried 128 commits of base movement including breaking spec changes
  • pnpm --filter @objectstack/objectql test303 files / 5057 tests passed
  • pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 over the package-door / artifact population, enumerated by a name match on packages/runtime for package or artifact so the population is reproducible — 39 files / 512 tests passed. ⚠️ The first attempt exited 1 in 2 seconds and is recorded as NOT a red: the paths were repo-root-relative while pnpm exec runs at the package root, and the repo's own guard said so in words (FILTER SELECTED NOTHING — 39 of the 39 path(s) you named will run no tests). Re-run with package-relative paths for the reading above.
  • pnpm --filter @objectstack/spec --filter @objectstack/objectql typecheck — exit 0; both test layers compile (spec 53 files / 257 errors / 142 pins; objectql 40 / 234 / 65, unchanged). ⚠️ The spec ledger moved from 54 / 259 / 144 by main's feat(spec): the /packages doors declare the query parameters they execute, and retire the two they never did #19364 arriving in a merge, ⛔ not by this PR.
  • pnpm --filter @objectstack/spec --filter @objectstack/objectql typecheck — both exit 0 on this head; the debt ledgers held shrink-only (spec 53 files / 257 errors / 142 pinned signatures; objectql 40 / 234 / 65).
  • pnpm --filter @objectstack/spec build exit 0 (34/34 declared .d.ts present, check-dts-references resolved 378/378), and the whole @objectstack/runtime dependency closure was rebuilt first, so nothing below read a dist stale against 128 commits of main.

Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived from this tree, every command run with its exit code written to a file, reconciled with --ran: 116 derived, 114 run, 2 NOT-MEASURED, 0 UNRUN, and the tool's own verdict line says so. 113 exit 0. The two NOT-MEASURED are the tool's DERIVED classification of an exit 3; a third measured nothing too, and the tool cannot see it because its refusal code is 2. ⛔ None of the three is a finding:

  • check:dual-build-cjs-loads — exit 3, its own PREREQUISITE NOT MET … ⛔ This is NOT a pass: nothing was measured (66 packages have no dist; it wants a whole-repo build).
  • check:type-check-debt — exit 3, same shape, same wording, wants the full package closure built.
  • check-engine-split-ratio --days 90 — exit 2, refuses on a shallow clone whose oldest visible commit sits inside the 90-day window. It says a ratio derived there would be 「real, plausible and WRONG」.

A fourth, check:skill-examples, first exited 1 on an unbuilt packages/client-react; after building that package it re-runs green — 258 prose examples type-check across 3 surfaces. Both readings are stated here, and the reconciliation record carries ONE of them — the green re-run — because the tool flags a doubly-recorded family and says to make the record state one thing. The re-derivation on the final head yields 116 families: check:api-surface-declarations is gone (retired upstream by #19024 mid-round) and check:gitlink-declared is new, run green. No family is left unrun.

Ratchet families re-run after the last merge, on 96dd3549ff6: check:generated (all 15 artifacts up to date), check:api-surface, check:authorable-surface, check:export-origins, check:declaration-map, check:docs, check:skill-refs, check:entry-nameability, check:dual-source-exports, check:spec-changes, check:spec-parsed-alias, check:published-files, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage — all exit 0. Control characters: grep -naP over every file I hand-edited returns nothing (exit 1).

Generated artefacts in this diff, and why each moved

  • json-schema.manifest/automation.json, authorable-surface/automation.json, authorable-defaults/automation.json, api-surface/*, export-origins/*, declaration-map/automation.json, content/docs/references/** — the new exports, regenerated by the package's own gen: scripts. authorable-defaults records automation/FlowFunctionLoweredDeclaration:effect = "pure", which is the confidence-gap reading in ledger form.
  • packages/spec/dropped-refinements.baseline.json — four api/* entries each gain one site (…manifest.hooks.element.object), counts 569 → 573. Cause: the record stage declares hooks where z.unknown() declared nothing, so HookSchema's object refinement now reaches the runtime and not the published file. The ledger is hand-edited by design and the build printed the exact delta.
  • skills/objectstack-platform/references/_index.md — one generated line listing stack.zod.ts's exports.

skills/** readings, and the landing tier

This diff touches skills/objectstack-platform/references/_index.md, so the PR is governed, Tier H on its file list. ⛔ It stays a draft and no AI seat merges, queues or arms auto-merge on it.

Both readings the skills rule requires, at merge base c334ba0f3a6:

  • changed file, whole file: 41 lines before, 41 after — net 0. The diff is one regenerated line.
  • package total (sum of every SKILL.md): 6145 before, 6145 after — net 0.

node scripts/check-skills-token-ratchet.mjs exits 0 and classifies this file as generator-owned (measured, not ratcheted), so no authored ceiling is charged.

Clause ②, and the changeset is not one package's

Clause-②: yes, and two changesets because two published packages move:

  • @objectstack/specminor. New exports, and the two installed-package responses move from z.unknown() on functions / hooks to declared JSON shapes. That is a narrowing on a published declaration; what it does NOT withdraw is measured, on real producers: the showcase shape, the array form and the already-lowered body an artifact boot installs all parse.
  • @objectstack/objectqlpatch. GET /packages reports functions it previously dropped. No API is added or removed; a read door stops under-reporting. Grade it up if a payload gaining entries reads as minor to the reviewer.

Serial and merge state, re-taken by this seat

Changed-file map re-taken first-hand over all 33 open PRs (271 file rows) rather than inherited. LIT control packages/spec/src/ui/action-params.zod.ts resolves to #19315; DARK control packages/spec/src/zzz-no-such.zod.ts resolves to nothing.

origin/main has been merged six times on this branch. #19024 (which retired api-surface-declarations/) came in early, which is why no api-surface-declarations/*.txt appears in this diff. The fifth merge brought #19363, a BREAKING spec change. The sixth merge, the head of this body, brought 128 commits — so the full spec suite was re-run rather than only the generated gates.

scripts/pm/os-regen-merge.sh was NOT used in either round — its rerun arm is re-entrant and commits a revert of the operator's own regeneration, filed as #19392. Steps 1–3 of its documented order were performed by hand, against a merge base captured BEFORE the merge and an origin/main fetched into an OWNED ref so a sibling's fetch could not move the target mid-round.

The sixth merge decided THREE paths, and only one of them was a conflict. That gap is worth stating, because resolving only what a conflict probe names would have landed a silent loss:

path routed what the merge did how it was resolved
content/docs/references/index.mdx merge=os-regen driver deferred it, exit 0 — main's side silently dropped (merged blob 6290447bd9a == ours, != theirs 7e1f9b6f13e) main's side restored into the WORKING TREE ONLY, then regenerated whole
content/docs/references/api/package-api.mdx merge=os-regen same — main's side silently dropped (merged 988bedaa480 == ours, != theirs d09cd420711) same
packages/spec/dropped-refinements.baseline.json not routed exit 1 — the only real text conflict, one hunk, confined to three summary counters in the measured header both sides' entries unioned, then the build adjudicated

⚠️ package-api.mdx appears in NO conflict list and never could. It text-merges cleanly driver-free, so a GitHub-condition probe cannot name it; only the both-edited ROUTED set, computed per file against the pre-merge base, finds it — which is exactly what os-regen-merge.sh step 2 specifies and what the driver's own $GIT_DIR/os-regen-pending record listed.

The regenerated docs are the UNION, proven in both directions (added/removed line multisets compared as sets): package-api.mdx identical at 20 and 14 lines; index.mdx identical at 12 and 6 lines, excluding the two running-total lines — a union MUST move a total neither side moves alone, so their disagreement is the signature of a correct union rather than a failure, and the line counts already matched (16/16, 10/10) before excluding them. The total is re-derived, not arithmetic: base 1533, this branch alone 1534, main alone 1534, merged tree 1535, and 1535 is what gen:schema itself reports for the merged sources. Main brought DatasetSelection, DatasetCompareTo and DatasetTotals and retired KernelSecurityScanResult / KernelSecurityVulnerability; this branch brought FlowFunctionLoweredDeclaration. All survive, asserted through the published export map of the freshly built dist with a dark control (an invented export name reads undefined).

The ledger was resolved by hand, and that is the only route available. dropped-refinements.baseline.json is hand-edited BY DESIGN with no gen: script — its own description states why: "a generator would let a new gap be admitted by running a command instead of by a decision, which is the silence this ledger exists to end." The build VALIDATES it bidirectionally and refuses; it never writes it. Both sides' entries were unioned (union keys missing from the merged file: none; merged keys not in the union: none; api/DatasetSelection arrived from main via #19638 and survives; main's removal of the fields.out.keyType sites is kept — nine site lines at the merge base, zero at this head and zero on main (lit control: 204 "sites" keys at base; dark control 0). ⚠️ The merge round's own prose said five; that was a narrative miscount caught by the merge-delta review and re-counted by the seat. The FILE was always right), then gen:schema adjudicated and measured 565 dropped sites across 205 published schemas — the union as resolved. One counter the build corrected: refinementSitesThatDidProject read 357 and the build measures 366.

⚠️ That correction is filed as #19681, because nothing in the repository would have caught it: two of the four measured counters have no reader anywhere (lit control — the other two have two readers each, dark control 0), so they can hold any number and every gate stays green.

Acceptance notes

  • noted, not filed: regenerating packages/spec/api-surface-declarations/ui.txt produced a 184-line change that is a pure permutation of its own content — the same union members in a different order, 0 removed, 0 added, 35 reshaped. Verified as a precedented shape rather than a defect: commit 24d622b94b8, a spec change touching zero files under packages/spec/src/ui/, moved the same file by 5 lines whose sorted content is byte-identical. The whole artefact was retired upstream by revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 mid-round, so nothing of it survives in this diff and the population is gone. Carrier: none — the file no longer exists.
  • noted, not filed: packages/objectql's tests resolve @objectstack/metadata-protocol from dist, so after merging upstream The in-process installPackage door ignores the enableOnInstall its own request schema declares — honour it the way the HTTP door does (successor of #18605, ruling batch #153 item 5 letter 1) #19277 the seven assertions in protocol-install-package-enable-on-install.test.ts failed against a stale build of a package this PR never touches; building that one package turns all seven green. A local-environment reading, not a repo defect, and check:test-source-alias already owns the aliased/unaliased ledger this sits in. Carrier: the next seat that runs objectql's suite after a merge — it will see the same red and should build the dependency before reading it as a finding.

维护者速读(草稿)

改了什么 —— 一个包的「包体」在平台里其实要经过四个阶段:作者写的、内存里装配好的、落盘成 artifact 的、注册表记录下来的。前两个早有声明,后两个从来没有。这次把后两个补上:ArtifactStagePackageBodySchema(落盘 artifact)和 RecordStagePackageBodySchema(注册表记录),放在既有的装配体旁边,装配体一个字不动。同时修好一个生产者缺陷:GET /packages 以前会把「裸写的函数」整条漏报,现在两种写法都报。

为什么改 —— 两件事各有代价。其一,装配体里有两个键(functionshooks)声明了「可以是一个活的函数」,而 JSON Schema 表达不了函数,于是任何嵌入它的接口都会整份丢掉自己的 JSON Schema;读 API 只能把这两个键写成「什么都收、不检查」。其二,我们自己发布的 showcase 声明了 2 个函数,而 GET /packages 只报 1 个——机器可读的读门把事实说少了。

风险与代价(含回滚) —— 风险集中在一处:那两个键从「什么都收」变成「按声明收」,理论上可能拒掉今天能读的行。已实测三种真实生产者(showcase 的写法、数组写法、artifact 启动装回来的写法)全部照常通过,并且用两次消融证明了这些断言真的会红而不是摆设。⛔ 装配体与 composeStacks 未动,所以 os dev / os serve 的行为不受影响——这正是上一版裁决 B 被撤回的原因,这次没有重蹈。回滚:两个 spec 改动与 objectql 改动互相独立,git revert 任一半都不会让另一半变红;最小回滚是把 package-api.zod.ts 的那一行绑回装配体,新声明留着不用。

席位意见 ——

你要做的 —— 这个 PR 的文件里有一份 skills/** 的生成文件,按规则整单属于 Tier H,只有你(或你授权的批准)能让它落地;AI 席位不会合并、不会排队、不会解除 draft。请看两点:① @objectstack/objectql 我打的是 patch,理由是「读门修复、不增删 API」,若你认为「载荷多出条目」应算 minor,说一声即可改;② functions 的声明式阶段我按「两种 lowered 写法都收」实现(理由写在上面第 1 条),如果裁决本意是只收记录式那一种,也请直接说,那会让 objectstack build 今天写出的一种 artifact 被拒。


Generated by Claude Code


Generated by Claude Code

…rd package body stages

Four stages, four declarations: authoring, in-memory assembled, on-disk
artifact, registry record. The last two had no declaration until now.

- packages/spec/src/automation/flow-function.zod.ts exports
  FlowFunctionLoweredDeclarationSchema, the serialisable half of the pair.
- packages/spec/src/stack.zod.ts declares ArtifactStagePackageBodySchema and
  RecordStagePackageBodySchema BESIDE AssembledPackageBodySchema, which is
  untouched (composeStacks keeps building bodies that hold live callables).
- packages/spec/src/api/package-api.zod.ts rebinds the installed-package row's
  manifest to the record stage; the z.unknown() override and the docblock
  defending it are gone.
- packages/objectql/src/registry.ts normalises a bare callable functions entry
  to the declared form at the assembly boundary, so the structural projection
  reports every declared function instead of dropping the bare ones.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
…s, and fix the record-copy cast

- json-schema.manifest / authorable-surface / authorable-defaults gain
  automation/FlowFunctionLoweredDeclaration: the lowered record now publishes,
  which is what unemitted-schemas.baseline.json already claimed about it.
- dropped-refinements.baseline.json gains one site per installed-package
  response: the record stage DECLARES hooks where z.unknown() declared nothing,
  so HookSchema's `object` refinement now reaches the runtime and not the file.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
…declaration's type aliases

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
…sembled-package-body-inert-json

# Conflicts:
#	packages/spec/api-surface-declarations/api.txt
#	packages/spec/api-surface-declarations/automation.txt
#	packages/spec/api-surface-declarations/root.txt
#	packages/spec/api-surface-declarations/ui.txt
#	packages/spec/dropped-refinements.baseline.json
…dropped-refinements ledger

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 20, 2026
@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec, touching 18 documentable anchor(s). ⚠️ 9 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/api-surface/root.json, packages/spec/authorable-defaults/automation.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/metadata-service.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))
  • content/docs/protocol/kernel/error-handling.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via installPackage (symbol, a method of class SchemaRegistry))
  • content/docs/releases/v17/17-0.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))
  • content/docs/releases/v17/17-4.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 9 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/api-surface/root.json, packages/spec/authorable-defaults/automation.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 97f4f8c8282ebb78de0815cec24eddb8b8335eb0packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8ca4fe8711d721cb5ed90d53a4813376d3205cef — the merge of head 96dd3549ff68924bcc665fe3209542d44257a6c2 into base 97f4f8c8282ebb78de0815cec24eddb8b8335eb0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8ca4fe8711d721cb5ed90d53a4813376d3205cef && git checkout 8ca4fe8711d721cb5ed90d53a4813376d3205cef
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 97f4f8c8282ebb78de0815cec24eddb8b8335eb0 96dd3549ff68924bcc665fe3209542d44257a6c2 && git checkout -B drift-repro 97f4f8c8282ebb78de0815cec24eddb8b8335eb0 && git merge --no-ff 96dd3549ff68924bcc665fe3209542d44257a6c2

node scripts/docs-audit/affected-docs.mjs --json 97f4f8c8282ebb78de0815cec24eddb8b8335eb0

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 97f4f8c8282ebb78de0815cec24eddb8b8335eb0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…sembled-package-body-inert-json

One conflicted path, packages/spec/src/api/package-api.zod.ts, and the overlap is
the import block alone: main's side adds `retiredKey`, this branch rebinds the row
manifest's import from `AssembledPackageBodySchema` to `RecordStagePackageBodySchema`.
Both imports kept; the assembled-body import is dropped because nothing in the merged
file references that symbol in code any more (three prose mentions in the docblock only).

Measured: the merged file equals main's side plus this branch's exact delta, and equals
this branch's side plus main's exact delta -- added/removed line multisets identical in
both directions.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
Discharges the os-regen deferral taken on the merge commit. Step 2 restored
main's side of content/docs/references/api/package-api.mdx (both sides moved it,
so the driver had silently kept one); this regeneration re-derives this branch's
generated content on top of it, which is why the `functions` and `hooks` rows
read as the record-stage declaration again instead of `any`, while main's retired
`limit`/`cursor` query-parameter rows stay.

`pnpm --filter @objectstack/spec check:generated`: all 15 generated artifacts up
to date.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
… side of

`scripts/pm/os-regen-merge.sh` was rerun once more after the regeneration commit
had landed. Its step 2 is not idempotent across that boundary: with the branch's
regenerated bytes in HEAD the "both sides changed it" test now fires for
content/docs/references/api/package-api.mdx and content/docs/references/index.mdx,
so it restored main's side of both and committed it -- erasing
`FlowFunctionLoweredDeclaration` from the automation listing (schema count
1533 -> 1532) and rolling the package-api `functions`/`hooks` rows back to `any`.

This commit re-runs `gen:schema && gen:docs` on the merged tree. The result is
byte-identical to the regeneration commit: `git diff 3a7ab9c -- content/` is
empty.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
Discharges the os-regen deferral the merge commit recorded. `origin/main`
and this branch both moved content/docs/references/index.mdx, so the driver
resolved it with exit 0 and silently kept one side -- measured: the merged
blob was byte-identical to this branch's, and main's side (the UI section's
`DashboardWidgetChartConfig` row and its counts) was gone.

Step 2 restored main's side into the working tree only, and this commit is
`gen:schema && gen:docs` re-derived on top of it. The result is the union of
both intents: the UI module reads 16 pages / 158 schemas with
`DashboardWidgetChartConfig` listed, the automation module reads 14 pages /
75 schemas with `FlowFunctionLoweredDeclaration` listed, and the total moves
1533 -> 1534. Against each side separately the regenerated file differs by
exactly the other side's delta and by nothing else.

No hand edit: the file's own header routes it to
`pnpm --filter @objectstack/spec gen:schema && gen:docs`.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: aac764cc36113b4e52820c1695715f000ccbe1b4

① Derived judgments

Governing ruling, established from the thread rather than the PR body. The card carries four ruling records. 5651572469 (batch #127 item 4, 2026-09-13, maintainer 「其他同意」) prescribed an artifact-stage body BESIDE the assembled one and the read-row rebind. 5716259259 (batch #149 item 1 letter B, 09-17) was presented after a pm:retriage (5706769218 / 5706819210) that did not cite #127, and prescribed the opposite: narrow the assembled body through entry-schema variants. Round R4 measured B as unexecutable (it refuses the in-repo fixture composeStacks builds today, contradicts the 「cannot drift」 invariant, and cannot meet its own acceptance because the inline array member keeps a z.function() handler) and delivered an empty diff by design (Release 5728544298). 5729478920 (batch #159 item 2 letter A, 09-18) restated #127 and WITHDREW B. Round R5 then stalled at A's own step 4: toRecordManifest leaves a residual that is neither a string nor a lowered record, and the producer is packages/objectql/src/registry.ts, outside the surface A drew — second empty diff (Release 5729976367). 5748934194 (batch #192 item 3 letter A′, 2026-09-20, maintainer 「192 同意」) is A plus the mechanism for step 4: a record-stage body with functions[].handler optional, the read rows rebound to THAT stage, and a bare-callable normalisation in objectql at the assembly boundary, ⛔ no minted ref, ⛔ no dropped entry. A′ governs the delivered diff, and the diff executes it step for step. The two places the PR body says it read the ruling rather than transcribed it are judged below.

What @objectstack/spec gains at this head (read from api-surface/, export-origins/, json-schema.manifest/, authorable-*/ and the source, not the PR body): root exports ArtifactStagePackageBodySchema, RecordStagePackageBodySchema and the four …Body / …BodyParsed type aliases; automation exports FlowFunctionLoweredDeclarationSchema with FlowFunctionLoweredDeclaration / …Parsed. json-schema.manifest/automation.json gains automation/FlowFunctionLoweredDeclaration; authorable-surface gains its handler and effect; authorable-defaults records effect = "pure"; the reference docs gain the section and index.mdx counts 1534. declaration-map/automation.json newly maps the AUTHORING FlowFunctionDeclarationSchema to automation/FlowFunctionLoweredDeclaration — that is the generator's documented base-pass rule (it follows the .extend() receiver of an exported def), not a hand edit, and it collides with nothing because the authoring declaration has no def of its own. Nothing exported is lost; the only removal is the module-private AssembledPackageRecordBodySchema const in package-api.zod.ts, which was never exported.

What changes. AssembledInstalledPackageSchema.manifest — the row both ListInstalledPackagesResponse and GetInstalledPackageResponse carry through InstalledPackageAtEitherStageSchema — rebinds from the assembled body with two z.unknown() holes to RecordStagePackageBodySchema. The published JSON Schema of the two responses therefore declares functions and hooks in full (the generated content/docs/references/api/package-api.mdx rows move from any to the lowered shapes). dropped-refinements.baseline.json gains four sites, all …manifest.hooks.element.object: that is HookSchema.object's pre-existing .refine() (hook.zod.ts:220) now reaching the runtime through a declared key while staying out of the JSON output — the ledger doing its job, not a weakened gate.

Is AssembledPackageBodySchema genuinely untouched? Yes, measured two ways. git diff 4b58dcf96..aac764cc3 -- packages/spec/src/stack.zod.ts is one widened import line plus a 142-line pure insertion between the AssembledPackageBodyParsed alias and ArtifactPackageSchema; AssembledPackageBodySchema (1283-1286), assembledPackageBodyShape (1167), composeStacks and the 「cannot drift」 invariant (3974) are byte-identical to the base. Both fenced files (data/hook.zod.ts, migrations/registry.ts) have an empty diff. Executed in an exported copy of this head (never in the clone): compose-stacks-manifest-preserve.test.ts + assembled-package-body.test.ts + the two new or changed spec test files — 4 files / 120 tests green. My own probe: the assembled body accepts a live callable in the map form, the array form and hooks[].handler, and both JSON stages refuse all three.

Do the two stage schemas admit what objectstack build and SchemaRegistry.installPackage really write? packages/cli/src/utils/lower-callables.ts:254-265 writes out[ref] = ref (a bare string) for a bare entry and { ...value, handler: ref } for a declared one; :188-204 lowers hooks[].handler to a string. Probe on the artifact stage: bare ref string accepted, lowered record accepted, callable refused, { effect } residual refused, empty-string ref refused. installPackage has exactly ONE record-writing site (registry.ts:4301, toRecordManifest(withDeclaredFunctionEntries(manifest))), so no update or upgrade path bypasses the normaliser. Probe on the record stage: { effect: 'writes' } accepted, {} accepted and parses to { effect: 'pure' }, array { name, effect? } and { name } accepted, string and lowered record accepted, callables refused. Hooks: a projected hook with neither handler nor body is accepted by all three stages because base HookSchema.handler is already .optional() (hook.zod.ts:252, deprecated in favour of body), so the PR's z.string().optional() inherits that optionality rather than adding it — ruling A′'s 「hooks[].handler a string」 is met. Strictness parity: a map entry with an unknown key is refused by all three stages (the lowered form derives from the strictObject declaration); an ARRAY entry with an unknown key is stripped by all three, because the authoring array member is plain z.object (stack.zod.ts:648) and the transcription matches it — no new tolerance in either direction.

OPEN QUESTION 1 — admitting both lowered spellings. Faithful, and I would have failed the other reading. The ruling's phrase 「functions entries the lowered declaration」 is shorthand for the lowered members of FlowFunctionEntrySchema, which are exactly two (flow-function.zod.ts:265-270: the bare ref string and FlowFunctionLoweredDeclarationSchema); ruling A's step 4, which A′ inherits verbatim, names the legitimate wire shapes as 「a string or a lowered record」. A stage admitting only the record form would refuse every artifact objectstack build writes for a bare function — the class of failure that withdrew B, one key across. Nothing in the tree supports the single-form reading.

Does the objectql change alter what GET /packages serves, and how? Yes, additively. withDeclaredFunctionEntries rewrites a bare callable map entry to { handler: fn, effect: DEFAULT_FLOW_FUNCTION_EFFECT } before the structural projection, which then leaves { effect: 'pure' } — the same value normalizeFlowFunctionEntry assigns a bare callable at boot (flow-function.zod.ts:324), so the record reports what the runtime actually does with the entry, not a minted value. Rows for packages with bare-callable entries gain those keys; nothing is removed; the showcase row goes 1 → 2. The projection's structural rule is untouched, no ref is minted, and the caller's manifest is copied only when an entry was rewritten. No in-repo consumer reads manifest.functions off a registry row (packages/runtime/src/app-plugin.ts:2282 reads the live bundle via resolveArtifactCollections; lit control manifest.objects hits). One asymmetry, not a defect: a bare entry lands as { effect: 'pure' } explicitly, while a declared entry authored without effect lands as {}; both sit inside the record stage, which defaults the latter on parse.

The two ruling items read rather than transcribed, my verdict on each. (a) unemitted-schemas.baseline.json is not edited: its reason line for Automation.FlowFunctionDeclarationSchema says the lowered record 「publishes normally」, which was false while the const was module-local and is true now that automation/FlowFunctionLoweredDeclaration is in the manifest — A′'s correction is achieved by making the sentence true. Acceptable. (b) The package-api.zod.ts docblock's last sentence is corrected in place, and the correction is true: scripts/build-schemas.ts imports the subpath namespaces (../src/kernel, ../src/ui, …) and never ../src/stack.zod, so those two members were never why ArtifactPackageSchema / ObjectStackDefinitionSchema publish no JSON Schema.

One class of row moves from tolerated to refused-by-declaration: a map entry whose effect is neither pure nor writes (reachable only from a defineStack({ strict: false }) host) now fails the record stage where z.unknown() accepted it. That is a producer defect the declaration exists to name, and packages/runtime/src/domains/packages.ts builds its response directly (deps.success({ packages: rows, … })) with no schema parse on the wire, so it changes what the contract says, not what the door does.

② Semver level

The written rule (AGENTS.md 1064-1068 at origin/main): a bug fix in a released package takes patch; the declaration is Clause-②: yes|no with at most one arm; yes takes at least minor; (narrowing) is BREAKING and must carry its FROM → TO migration when it removes or renames anything an author can write. Clause ② itself is defined in .claude/skills/pm-dispatch/references/lanes/spec.md:21-22: widening the accept set or expanding the public surface, however small, is Clause ②; narrowing is still contract surface but does not trigger it, and declaring yes on it is never an error. SKILL.md:515: pulling behaviour back to its declared contract does not touch it.

  • @objectstack/spec: minor — correct. Nine new permanently published exports expand the public surface, so Clause-②: yes and at least minor. The read-row narrowing withdraws nothing an author can write: the values it now refuses are a live callable in a JSON row and an invalid effect, neither of which a valid producer emits (measured above on the build output, the projection residual and the array form), so no (narrowing) arm and no migration are owed. This is also exactly the grade ruling 🔗 Broken links detected in documentation #127 item 6 assigned to 「the read API's two keys narrow from anything to their declared shape」.
  • @objectstack/objectql: patch — correct under the written rule; this is my reading of OPEN QUESTION 2. No export is added, removed or renamed; the payload gains entries the previous declaration (z.unknown()) already permitted, so no consumer could have relied on their absence; the record is being pulled back to what the package declared and to the record stage, which SKILL.md:515 says is not Clause ②. That is a bug fix in a released package, and the rule assigns it patch. The PR-level Clause-②: yes is satisfied by the spec changeset; the rule does not require every changeset in a yes PR to be minor. Grading it minor would not be an error under lanes/spec.md:22, but it is not what the rule asks for, and ruling A′ asked only that the payload change be declared in the objectql changeset, which it is.

③ Boundary flags

  • Published contract semantics moved (handled). The two installed-package read responses now DECLARE manifest.functions and manifest.hooks instead of accepting anything. Every row the door really serves still parses (measured on the build output, the projection residual, the array form and a projected hook), and the door has no runtime response parse, so no runtime refusal that did not exist before is introduced. The four new dropped-refinements sites are recorded by name.
  • GET /packages payload change (handled, declared in the objectql changeset). Additive; nothing that was on the wire leaves it.
  • Tier H surface, for the maintainer's hand. skills/objectstack-platform/references/_index.md moves one generated line (net 0 tokens, check:skill-refs green). Because the Exports: fallback lists the first five non-constant exports in source order (packages/spec/scripts/lib/export-list.ts, slice(0, 5)), the stack.zod.ts pointer row now names ArtifactStagePackageBodySchema and RecordStagePackageBodySchema and NO LONGER names ArtifactPackageSchema or ObjectStackDefinitionSchema. That is the generator's rule rather than this PR's editing and is not a reason to fail, but it is a visible change to a customer-facing skill index, and the two names that dropped are the file's most consequential exports. A module doc block on stack.zod.ts in a separate change would stop the row depending on export order.
  • No security or permission boundary, no migration or retirement shape, no gate weakened. AssembledPackageBodySchema, composeStacks, the drift invariant and both fenced files are untouched.

Measurement notes. The two new schemas carry the structural annotation z.ZodType(Record(string, unknown), Record(string, unknown)) — written here with parentheses in place of angle brackets — the same as the assembled body. Executed locally on an exported copy of this head: 4 spec test files / 120 tests, and a 25-row probe of the three stage schemas with lit and dark controls (z.string() converts, z.object({ a: z.function() }) does not; 55 / 55 / 55 shape members). NOT MEASURED locally: the @objectstack/objectql suite and the runtime conformance files — execution evidence for those is the 42 check runs on this sha (38 success, 4 skipped) read from the API — and the check:generated family, read from the diff and the same check runs.

Implemented-by: claude/issue-17518-assembled-package-body-inert-json
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

…t-json

Third merge round. main moved 128 commits past the merge base 4b58dcf.

Two paths needed a decision:

- content/docs/references/index.mdx and content/docs/references/api/package-api.mdx
  are merge=os-regen routed. The driver resolved both with exit 0 and silently kept
  THIS branch's side, dropping main's. Measured by blob hash against both sides, not
  assumed. main's side is restored into the working tree only; the regeneration
  commit that follows re-derives both from the merged sources.

- packages/spec/dropped-refinements.baseline.json is NOT driver-routed; it is a
  hand-edited, shrink-only ledger with no gen: script. Its only textual conflict was
  the three summary counters in the `measured` header; the `entries` body merged
  cleanly and holds the union of both sides (205 entries = 204 ours + main's
  api/DatasetSelection; main's repair of the fields.out.keyType sites kept). The two
  body-derived counters are recomputed here; refinementSitesThatDidProject is a
  measurement and is corrected in the regeneration commit.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
Discharges the os-regen deferral recorded by the merge commit. Both routed
paths are regenerated whole from the merged sources; no byte is hand-edited.

Union proof, both directions, per path:

  content/docs/references/api/package-api.mdx
    regen vs this branch's side == main's delta   (20 lines, identical)
    regen vs main's side        == this branch's  (14 lines, identical)

  content/docs/references/index.mdx
    regen vs this branch's side == main's delta   (12 lines, identical)
    regen vs main's side        == this branch's  (6 lines, identical)
    The two lines excluded from that comparison carry the running schema TOTAL,
    which a union must move where neither side alone does: base 1533, each side
    alone 1534, merged tree 1535 — and 1535 is what the generator itself reports
    for the merged sources, so the total is measured rather than reconciled.

  main brought   DatasetSelection/DatasetCompareTo/DatasetTotals into api/analytics
                 and retired KernelSecurityScanResult/KernelSecurityVulnerability
                 from kernel/plugin-security-advanced.
  this branch brought FlowFunctionLoweredDeclaration into automation/flow-function.
  Both survive in the merged output.

The ledger's last header counter is the build's own reading of the merged tree:
gen:schema measures 565 dropped sites across 205 published schemas, 366 projected,
9 with no JSON form — the first two already matched the union resolved in the merge
commit, so only refinementSitesThatDidProject moved.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 96dd3549ff68924bcc665fe3209542d44257a6c2

Scope: the DELTA between the previously PASSed head aac764cc361 (comment 5751097514) and this head — one merge of origin/main (7ba5f7f9e6f, 128 commits past merge base 4b58dcf96b3, both parents verified) and one regeneration commit (96dd3549ff6). The feature was reviewed before; this record judges what the merge did to it. Every reading below was taken from the tree, a fresh export of this head with its own dependency install, and the GitHub API — ⛔ not from the round's report, which I read afterwards to compare.

① Derived judgments

The PR's effective diff against main is the same 22 files, and on every both-touched file it is the same change the earlier PASS reviewed. git diff --name-only main..head and git diff --name-only base..aac764cc361 are the identical 22-path set (0 gained, 0 lost). Six of them were also edited by main: content/docs/references/api/package-api.mdx, content/docs/references/index.mdx, packages/spec/dropped-refinements.baseline.json, packages/spec/src/api/package-api.test.ts, packages/spec/src/api/package-api.zod.ts, packages/spec/src/stack.zod.ts. For each I ran a two-direction union proof on the added/removed line multisets — direction A: (head vs this branch's side) must equal (base vs main); direction B: (head vs main) must equal (base vs this branch's side). The three source files and package-api.mdx are IDENTICAL multisets in both directions. So the PR's change to stack.zod.ts against current main is still one widened import plus a pure 142-line insertion (+143/-1), the package-api.zod.ts rebind is still +29/-38 and the test file +64/-7 — byte-for-byte the changes the earlier record judged. Main's own edits to those files are orthogonal: #19595 rewrote the limit/cursor tombstone prose to name the enabled filter, #18319 repaired test fixtures to reverse-domain ids, and #19314 sits at stack.zod.ts line 3426 onward in the declaresCollection/composeStacks region, naming neither functions nor hooks. packages/objectql/src/registry.ts was not touched by main at all.

Finding ① — the second deferred os-regen path. Verified, and the drop was real: at the merge commit the package-api.mdx blob is 988bedaa480, identical to this branch's side and not main's d09cd420711; a grep of the merge-commit revision for main's two contributions (the reverse-domain id description, the enabled tombstone) reads 0 / 0. At this head the same greps read 8 / 2, this branch's declared functions rows read 2, and the dark control (any-typed functions rows) reads 0. The union proof is exact in both directions (+10/-10 and +7/-7). Independently, pnpm --filter @objectstack/spec check:generated on my export of this head exits 0 with check:docs over content/docs/references/** green, so the committed file is what the generator emits from the merged sources. The driver's own docblock (scripts/git-merge-regen.mjs lines 40-46) confirms the mechanism: defer, exit 0, record in os-regen-pending, and the content left behind is OURS by construction. Nothing main landed in that file was lost.

Finding ② — the hand-resolved ledger. Verified as a set operation across base, ours, theirs, merge commit and head. Keys: base 204, ours 204 (0 added, 0 removed), main 205 (api/DatasetSelection added, 0 removed), head 205 = the exact union (0 missing, 0 extra). Per-entry sites reconciled against the expected union (base minus each side's removals plus each side's additions): 0 mismatches over all 205 entries; the four entries this branch adds a manifest.hooks.element.object site to are also four of the entries main repaired, and head carries both edits on each. api/DatasetSelection is present at head and absent from this branch's side, so it arrived from main. Main's fields.out.keyType repair: NINE entries carried such a site at base (the round's narrative said five — a miscount in the prose, not in the file), main removed all nine, head has zero — the removal survived in full. Header: 205 / 565 match the body I counted; refinementSitesThatDidProject was 357 at the merge commit (this branch's stale value) and is 366 at head. I ran pnpm --filter @objectstack/spec gen:schema on my export: exit 0, and it prints 565 refinement site(s) across 205 published schema(s), 366 refinement site(s) DID reach the file, 9 had no JSON form — the four header numbers, re-derived by the instrument that owns them. That run is also the ledger's bidirectional validator (checkDroppedRefinements in scripts/lib/dropped-refinements.ts reports undeclared, miscounted with added/removed/length, repaired and vanished), so its exit 0 means the hand-merged entries equal the observed census path by path. Confirmed the round's design claim: the docblock at line 68 states the ledger is hand-edited with no gen: script by decision, no writer exists anywhere in the tree (git grep over scripts/, packages/spec/scripts/, packages/spec/package.json), and readDroppedRefinementsBaseline never reads measured. ⚠️ Consequence, stated rather than glossed: refinementSitesThatDidProject has no reader and no pin, so nothing in the repo would have caught 357 — the round's own out-of-scope finding is correct and I concur; it is pre-existing, not introduced here.

Finding ③ — the running total 1535. The reasoning holds: base 1533, each side alone 1534, and a union of two disjoint +1 changes must land on 1535, so the two total lines cannot satisfy a single-side-delta test and their disagreement is the signature of a correct union. Measured three independent ways rather than argued: (a) gen:schema on my export prints objectstack.json (1535 definitions); (b) packages/spec/json-schema.manifest/ — a set of sorted lists that git merged as a plain text union with no hand involved — sums to 1533 / 1534 / 1534 / 1535 at base / ours / main / head; (c) the per-module column of the index.mdx table at head sums to 1535 (API 444, Automation 75, Kernel 157, UI 158). On index.mdx the union proof is exact in both directions once those two lines are excluded (+8/-8, +5/-5), and the head lists DatasetSelection, DatasetCompareTo, DatasetTotals, DashboardWidgetChartConfig and FlowFunctionLoweredDeclaration once each while KernelSecurityScanResult and KernelSecurityVulnerability are absent; at the merge commit the control reads 0 DatasetSelection and 1 KernelSecurityScanResult, i.e. main's side really was missing there and is present now.

Both sides' load-bearing names, source AND built dist. Source and ledgers at head: ArtifactStagePackageBodySchema (3 src files, api-surface/root.json, export-origins/root.json), RecordStagePackageBodySchema (4, root, root), FlowFunctionLoweredDeclarationSchema (3, api-surface/automation.json, export-origins, json-schema.manifest/automation.json), DatasetSelectionSchema (5, api-surface/api.json, export-origins, manifest), KernelSecurityScanResultSchema / KernelSecurityVulnerabilitySchema 0 in all three ledgers — with the lit control that both WERE in api-surface/kernel.json and export-origins/kernel.json at the merge base, and the dark control ThisExportWasNeverAuthoredSchema 0 everywhere. Built dist: I built @objectstack/spec on my export (exit 0, 34/34 declaration files, 378/378 references resolved) and probed through the published export map from a workspace package that declares the dependency — 11/11 held: all six present names present in their entries, both retired names undefined, AssembledPackageBodySchema and KernelSecurityPolicySchema present as lit controls, the dark control undefined. The retirement's pin, packages/spec/src/kernel/plugin-security-scan-result-retirement.test.ts, asserts not.toHaveProperty for both retired names on the ./kernel barrel with six surviving neighbours as anti-vacuity, and the only remaining word-level mention of either …Schema name in packages/spec/src is that pin (the other mentions are the migration registry entries and prose). Main's retirement and its guard both survived.

Did the merge withdraw, weaken or alter anything the earlier PASS relied on? No, measured on the built dist of this head: 23/23 behaviour assertions held, with an instrument row (ManifestSchema accepts the fixture), lit and dark controls. The assembled body accepts a live callable and both JSON stages refuse it; both stages accept both lowered spellings; the record stage accepts a handler-less declaration and the artifact stage refuses it; {} parses on the record stage to effect: 'pure'; a string hook handler is accepted and a callable one refused; both JSON stages convert under z.toJSONSchema and the assembled body does not (z.string() lit, z.object({ a: z.function() }) dark); an unknown key is refused by all three. One thing I widened into because main's 128 commits include a BREAKING spec change: #18319 puts a reverse-domain rule on ManifestSchema.id. Both new stages are ManifestSchema.extend(...) derivations, the same construction as the assembled body, so the rule reaches Manifest / Assembled / Artifact / Record identically — measured false/false/false/false on NotReverseDomain, true on a conforming id (lit), false everywhere on an id with a space (dark). The merge did not produce a stage lagging main's change. unemitted-schemas.baseline.json was touched by neither side, so the reason line the earlier PASS relied on is unchanged.

CI on this sha: 42 check runs, 38 success, 4 skipped, 0 other.

② Semver level

The written rule (AGENTS.md, now at lines 1082-1097 on origin/main; the dispatch's 1055-1075 pointer predates main's 17 inserted lines) is byte-identical in that region between the merge base and main, so main brought no change to the grading itself. Because the PR's effective change on every source file is the identical multiset the earlier record graded, the readings stand at this head:

  • .changeset/17518-inert-json-package-body-stages.md@objectstack/spec: minor, still correct. Nine new permanently published exports (present in the dist at this head); Clause-②: yes demands at least minor. No (narrowing) arm is owed by this PR: main's #18319 narrowing of manifest.id reaches the two read responses through ManifestSchema regardless of this PR (the row's manifest was already ManifestSchema.extend(...) with two unknown holes), and it ships under main's own BREAKING changeset with its FROM → TO text — this PR neither adds to nor conceals it.
  • .changeset/17518-registry-record-reports-every-function.md@objectstack/objectql: patch, still correct. registry.ts is untouched by main, so the reasoning of the earlier record (a released read door pulled back to its declared contract, no export added or removed) is unchanged by the delta.

Both changeset files are byte-identical between aac764cc361 and this head, and the Check Changeset runs on this sha are green.

③ Boundary flags

  • Published contract semantics, handled and not this PR's own. Because the two JSON stages derive from ManifestSchema, the published JSON Schema of the two installed-package read responses now also carries main's reverse-domain id rule (#18319). It arrives by derivation, applies equally to the assembled body, and is declared in main's breaking changeset; nothing here to fix, but a maintainer reading the regenerated package-api.mdx will see both this PR's functions/hooks rows and main's id rows move in one diff.
  • Gate strength, pre-existing gap surfaced by this merge, handled. The ledger's refinementSitesThatDidProject counter sat at 357 in the merge commit while the tree measured 366, and no reader or pin would have caught it; it was corrected by hand from the generator's printed value and my own run confirms 366. No gate is weakened by this PR; the counter was never a gate. The round has filed it.
  • No security or permission boundary, no migration or retirement shape introduced by this delta (main's #15932 retirement arrives intact with its pin), no new runtime refusal from the merge: packages/runtime/src/domains/packages.ts still builds its response without a schema parse, and registry.ts is untouched by main.

Measurement notes. Type annotations are written here with PARENTHESES in place of angle brackets: the two new schemas carry z.ZodType(Record(string, unknown), Record(string, unknown)), the same annotation as the assembled body. Executed on a fresh export of this head (never in the clone): gen:schema exit 0, @objectstack/spec build exit 0, check:generated exit 0 (all 15 artifacts up to date against the dist I built), an 11-row dist export probe and a 23-row behaviour probe. NOT MEASURED locally: the @objectstack/spec, @objectstack/objectql and runtime test suites and both typechecks — execution evidence for those is the 42 check runs on this sha (38 success, 4 skipped) plus the round's reported counts, which I did not re-run; the driver-free mergeability of this head against main, for which I have GitHub's mergeable field only.

Implemented-by: claude/issue-17518-assembled-package-body-inert-json
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Provenance — carriers cleared, landing

Both clause-② carriers were removed by this seat at 2026-09-22T07:43Z, one limb then the other, three seconds apart — a clear, ⛔ not a strip. Each write went through label-write.mjs's four steps and read back:

Record consumed: comment 5772860458 on this PR — ## Contract review, Served-tier: CONTRACT_REVIEW_TIER, Head-sha: 96dd3549ff68924bcc665fe3209542d44257a6c2, VERDICT: PASS. It is a merge-delta review: the earlier PASS (5751097514) named aac764cc361, and the sixth merge moved the head, so a review naming the landing head was owed and taken.

Tier verified from the reviewing round's own transcript, ⛔ not from the dispatch parameter — counting the harness-stamped per-message served-model field:

probe count
"model":"claude-fable-5-1" (CONTRACT_REVIEW_TIER) 154
any claude-(opus|sonnet|haiku) id 0
total "model": occurrences 156

⚠️ The two unaccounted occurrences are named rather than rounded away: both are "model":{"description":… inside tool schema definitions, not served-model stamps. 154 of 154 stamps at tier; the arithmetic closes.

The record was adopted verbatim; ⛔ nothing in it was edited.

One number the review corrected, and this seat had propagated

The merge round's prose said main removed the fields.out.keyType sites from five entries. The review measured nine. Re-counted by this seat rather than taking either side:

ref fields.out.keyType site lines
merge base 4b58dcf96b3 9
head 96dd3549ff6 0
origin/main 0
lit control — "sites" keys at base 204
dark control 0

The FILE was always correct; only the narrative miscounted, and the PR body had inherited it. The body is corrected.

Landing state, all re-taken AFTER the last write

check reading
① review PASS naming the landing head 5772860458; --pair sees it as C6-RECORD
--pair 19373 exit 4 — one adverse row, C9, see below. ⛔ No carrier row is adverse
③ check runs on 96dd3549ff6 49 total — 43 success, 6 skipped, 0 adverse, 0 pending
mergeability mergeable: true, mergeable_state: clean
governed surface exit 3, tier H(人合), one path: skills/objectstack-platform/references/_index.md

The Tier H gate is lifted, by the register's own terms. Two APPROVED reviews by os-zhuang stand (5262113029, 5262584525). AGENTS.md:273-275, read first-hand:

Tier H … lift only for an authorized APPROVED review by an account in GOVERNED_APPROVERS, on ANY commit and not dismissed; that word is spent once per PR — the OWNING seat then lands it, later pushes included

⇒ both approvals sitting on aac764cc361 (now the pre-merge commit) still count — 「on ANY commit」 — and the sixth merge's push does not re-close the gate — 「later pushes included」. ⛔ This seat submitted no review, under any account.

⚠️ C9 is carried openly into the landing, ⛔ not worked around

--pair cannot reach 0 on this card: two live claim comments by different authors with no Release: between them, the earlier holder being a dev subagent session that ended 2026-09-12 and cannot post. The protocol's two exits are both unavailable, the checker forbids anyone writing that line on a holder's behalf, and the conflict between that prohibition and SKILL.md:496 is filed as #19400. Recorded on the card at 5751042169 and again at 5751467355.

C9 is a claim-bookkeeping row. It is not a carrier row, and Tier H's lift is the approval rather than a --pair zero. This seat is landing on the maintainer's direct instruction — quoted verbatim in 5772269872 — with the row named here rather than left for someone to find.

Next: auto-merge armed SQUASH, then followed to MERGED on origin/main with a lit and a dark control.


Generated by Claude Code

@os-litant
os-litant added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit eea7ccc Sep 22, 2026
53 checks passed
@os-litant
os-litant deleted the claude/issue-17518-assembled-package-body-inert-json branch September 22, 2026 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

3 participants